/**
 * Copyright © 2008, EMC Corporation.
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 *
 *     + Redistributions of source code must retain the above copyright notice, 
 *       this list of conditions and the following disclaimer.
 *     + Redistributions in binary form must reproduce the above copyright 
 *       notice, this list of conditions and the following disclaimer in the 
 *       documentation and/or other materials provided with the distribution.
 *     + The name of EMC Corporation may not be used to endorse or promote 
 *       products derived from this software without specific prior written 
 *       permission.
 *
 *      THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
 *      "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 
 *      TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
 *      PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS 
 *      BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
 *      CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
 *      SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
 *      INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 *      CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
 *      ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 *      POSSIBILITY OF SUCH DAMAGE.
 */
package com.emc.esu.api.rest {
    import com.adobe.net.URI;
    import com.adobe.net.URIEncodingBitmap;
    import com.hurlant.crypto.tls.TLSConfig;
    import com.hurlant.crypto.tls.TLSEngine;
    import com.hurlant.crypto.tls.TLSSecurityParameters;
    import com.hurlant.crypto.tls.TLSSocket;
    
    import flash.errors.EOFError;
    import flash.events.*;
    import flash.net.*;
    import flash.utils.ByteArray;
    import flash.utils.Timer;
    
    import mx.logging.ILogger;
    import mx.logging.Log;
    import mx.utils.StringUtil;


	/**
	 * Dispatched when the connection is closed
	 *
	 * @eventType flash.events.Event
	 */
	[Event(name="close", type="flash.events.Event")]
	/**
	 * Dispatched when the request has completed
	 * @eventType flash.events.Event.COMPLETE
	 */
	[Event(name="complete", type="flash.events.Event")]
	/**
	 * Dispatched if there is an IO error opening the socket or
	 * sending data.
	 * @eventType flash.events.IOErrorEvent.IO_ERROR
	 */
	[Event(name="ioError", type="flash.events.IOErrorEvent")]
	/**
	 * Dispatched if the request times out.
	 * @eventType flash.events.IOErrorEvent.NETWORK_ERROR
	 */
	[Event(name="networkError", type="flash.events.IOErrorEvent")]
	/**
	 * Dispatched if there is a security error opening the socket
	 * @eventType flash.events.SecurityErrorEvent.SECURITY_ERROR
	 */
	[Event(name="securityError", type="flash.events.SecurityErrorEvent")]
	/**
	 * Dispatched on load progress
	 * @eventType flash.events.ProgressEvent.PROGRESS
	 */
	[Event(name="progress", type="flash.events.ProgressEvent")]
	/**
	 * Dispatched when the HTTP status code and headers are available.
	 * @eventType flash.events.HTTPStatusEvent.HTTP_STATUS
	 */
	[Event(name="httpStatus", type="flash.events.HTTPStatusEvent")]
	/**
	 * The RestLoader is a custom HTTP client that uses the flash socket
	 * API to make requests.  It allows for a wider range of HTTP methods
	 * (PUT, DELETE) than the standard client and provides better 
	 * handling for request and response headers.  This functionality is
	 * required for proper handling of the full range of REST requests.  This
	 * class also implements all of its processing using ByteArray objects
	 * to prevent character conversion issues.  If you need the response
	 * body as a string, simply call 
	 * <code>loader.responseData.readUTFBytes( loader.responseData.length );</code>
	 */
    dynamic public class RestLoader extends EventDispatcher {
    	/**
    	 * Regular expression to detect HTTP response status
    	 */
    	private static const HTTP_STATUS:RegExp = /HTTP\/1.[01] ([0-9]{3}) ([A-Za-z0-9 ]+)/;
        // Path escaping bitmap
        protected static const ENCODED_PATH_CHARS:String = URI.URIpathEscape+"@";
        protected static const URIpathExcludedBitmap:URIEncodingBitmap =
                new URIEncodingBitmap( ENCODED_PATH_CHARS );

		               
		private var _socket:Socket;
		private var _uri:URI;
		private var _requestData:ByteArray;
		private var _requestHeaders:Array;
		private var _responseHeadersRead:Boolean = false;
		private var _responseHeaders:Object;
		private var _responseData:ByteArray;
		private var _bytesLoaded:int = 0;
		private var _bytesTotal:int = 0;
		private var _requestTimeOut:Number = 0;
		private var _timeoutTimer:Timer;
		private var _responseBuffer:ByteArray = new ByteArray();
		private var _httpStatus:int = 0;
		private var _statusLine:String = "";
		private var _requestMethod:String;
		private var _dataPump:Timer;
		private var _rawHeaders:String;
		private var _headerPos:int = 0;
		private var _complete:Boolean = false;
		private var _id:String;
		
	    private var logger:ILogger;
	    
		public static var traceEnabled:Boolean = false;
		private static var serial:uint = 0;
		
		/**
		 * Creates a new RestLoader
		 */
		public function RestLoader() {
			this.logger = Log.getLogger("EsuApi");
			_id = "RestLoader-" + (serial++);
			
		}
		
		public function get id():String {
			return _id;
		}
		
		/**
		 * The HTTP status code, e.g. 400
		 */
		public function get httpStatus():int {
			return this._httpStatus;
		}
		
		/**
		 * The HTTP status text sent with the status code,
		 * e.g. "Bad Request"
		 */
		public function get statusLine():String {
			return this._statusLine;
		}
		
		/**
		 * Gets the raw (unparsed) HTTP response headers.
		 */
		public function get rawHeaders():String {
			return this._rawHeaders;
		}
		
		/**
		 * The data contained in the body of the HTTP response
		 */
		public function get responseData():ByteArray {
	        return this._responseData;
		}
		public function set responseData( value:ByteArray ):void {
			this._responseData = value;
		}
		
		/**
		 * The data contained in the body of the HTTP request
		 */
		public function get requestData():ByteArray {
	        return this._requestData;
		}
		public function set requestData( value:ByteArray ):void {
			this._requestData = value;
		}
		
		/**
		 * The HTTP request headers
		 */
		public function get requestHeaders():Array {
	        return this._requestHeaders;
		}
		public function set requestHeaders( value:Array ):void {
			this._requestHeaders = value;
		}
		
		/**
		 * The request URI
		 */
		public function get uri():URI {
	        return this._uri;
		}
		public function set uri( value:URI ):void {
			this._uri = value;
		}
		   
		/**
		 * The request method used.  This is usually:
		 * GET, POST, PUT or DELETE.
		 */
		public function set method(method:String):void {
	        this._requestMethod = method;
		}
		
		/**
		 * Sets the request timeout in seconds.  If the connection has not
		 * completed before this time period expires, the connection will
		 * be severed.
		 */
		public function set requestTimeout( timeout:Number):void {
	        this._requestTimeOut = timeout;
		}
		
		/**
		 * The HTTP headers returned with the response
		 */
		public function get responseHeaders():Object {
	        return this._responseHeaders;
		}              
		
		/**
		 * The number of bytes loaded in the response body.
		 */
		public function get bytesLoaded():int {
	        return this._bytesLoaded;
		}
		
		
		/**
		 * Handles the connect event when the socket is opened.  Sends
		 * the HTTP request and headers.
		 */
		private function onConnectEvent(event:Event):void {      
			logger.debug( _id + ": CONNECT" );
	        sendHeaders();
	        dispatchEvent(new Event(Event.CONNECT));                
		}
		
		/**
		 * Handles the socket close event.
		 */
		private function onCloseEvent(event:Event):void {
			logger.debug( _id + ": CLOSE" );
			if( _dataPump != null ) {
	  			_dataPump.stop();
	  		}
	  		this._complete = true;

	        stopTimeout();
	        
	        if( !_responseHeadersRead ) {
	        	dispatchEvent( new IOErrorEvent( IOErrorEvent.IO_ERROR, false, false, "Socket closed by server before response read" ) );
	        } else {
		        dispatchEvent(new Event(flash.events.Event.COMPLETE));
	        }
	        
	        if( traceEnabled ) {
		        // Copy response data to prevent issues manipulating it 
		        // in event handlers.  Also truncate to 10k
		        var responseBody:ByteArray = new ByteArray();
		        var oldOffset:uint = _responseData.position;
		        _responseData.position = 0;
		        _responseData.readBytes( responseBody, 0, 
		        	_responseData.length>10240?10240:_responseData.length );
		        _responseData.position = oldOffset;
		        
		        var rawResponse:ByteArray = new ByteArray();
		        oldOffset = _responseBuffer.position;
		        _responseBuffer.position = 0;
		        _responseBuffer.readBytes( rawResponse, 0, 
		        	_responseBuffer.length>10240?10240:_responseBuffer.length );
		        _responseBuffer.position = oldOffset;
		        
		        TraceDispatcher.getInstance().dispatchEvent( 
		        	new RestResponseEvent( this, _httpStatus, 
		        	_statusLine, _responseHeaders, responseBody, 
		        	rawResponse ) );
	        }
		}
		
		/**
		 * Handles IO errors from the socket.  Forwards the error
		 * on to listeners.
		 */  
		private function onIOErrorEvent(event:IOErrorEvent):void {
			logger.debug( _id + ": " + event );
			if( _dataPump != null ) {
	  			_dataPump.stop();
	  		}
	  		this._complete = true;

	        stopTimeout();
			// For some reason, An IO Error
			// may be thrown way after everything is done.
			if( !this._complete ) {
	        	dispatchEvent(event);
	  		}
		}
		
		/**
		 * Handles security errors from the socket.  Forwards the error
		 * on to listeners.
		 */
		private function onSecurityErrorEvent(event:SecurityErrorEvent):void {
	        stopTimeout();
	       	if( _dataPump != null ) {
	  			_dataPump.stop();
	  		}

			// For some reason, A security
			// error is thrown way (many seconds) after everything is done.
			// I think it might have something to do with the socket getting closed,
			// but am not sure.
			// Error: Error #2048: Security sandbox violation: file:///E:/maui/wrappers/flex/testcases/bin-debug/esu_test.swf cannot load data from 192.168.1.110:8
			if( !this._complete ) {
				logger.debug( _id + ": " + event );
	        	dispatchEvent(event.clone());
	  		}
		}
		
		/**
		 * Handles messages from the socket that data is available.  Reads
		 * the data and parses it into the response headers and response body.
		 */
		private function onSocketDataEvent(event:ProgressEvent):void {
			logger.debug( _id + ": SOCKET_DATA" );
			stopTimeout();

			// loop while there are bytes available
			while( _socket.bytesAvailable ) {
				logger.debug( _id + ": " + _socket.bytesAvailable + " available" );
				//attempt to read from the _socket receive buffer
				try { 
					if( _responseHeadersRead ) {
						var read:int = _socket.bytesAvailable;
						logger.debug( _id + ": body: " + read + " bytes" );
						_socket.readBytes( _responseData, _bytesLoaded, read );
						_bytesLoaded += read;
						dispatchEvent(new ProgressEvent(ProgressEvent.PROGRESS, false, false, _bytesLoaded, _bytesTotal))
						continue;
					} else {
		                	                                 
		                var bytesRead:int = _socket.bytesAvailable;           
						_socket.readBytes( _responseBuffer, _headerPos, bytesRead );
						_headerPos += bytesRead;
		
						// Check for end of headers.
						var delimiterPos:int = findDelimiter();
						if( delimiterPos !=-1 ) {
							logger.debug( _id + ": Found header delimiter" );
							_responseHeadersRead = true;
		                         
							// Find end of header
							var headerData:ByteArray = new ByteArray();
							_responseBuffer.readBytes( headerData, 0, delimiterPos );
							headerData.position = 0;
							_rawHeaders = headerData.readUTFBytes( headerData.length );
							
							// Everything after the splitter goes to the response body
							var bodyBytes:int = _responseBuffer.length - (delimiterPos+4);
							_responseData.writeBytes( _responseBuffer, delimiterPos+4, bodyBytes );
							_bytesLoaded += bodyBytes;
		                       
							var t2:Array = _rawHeaders.split("\r\n");
		   
							_responseHeaders = {};
							for each(var header:String in t2) {
								if(header.indexOf("HTTP/1.")==-1) {
									var t3:Array = header.split(":");
									if (t3.length > 2) {
										var r:String = t3.shift();
                                        _responseHeaders[r] = StringUtil.trim(t3.join(':'));
		                            } else {
		                            	_responseHeaders[t3[0]] = StringUtil.trim(t3[1]);            
		                            }
		                        }
		                    }
		                       
		
                    	    if( _responseHeaders["Content-Length"] ) {
                                _bytesTotal = int( _responseHeaders["Content-Length"] );
                        	}
		                    
		                    // Checks for the HTTP status line
							if( HTTP_STATUS.test( _rawHeaders ) ) {
								var statusLine:Object = HTTP_STATUS.exec( _rawHeaders );
								var status:String = statusLine[1];
								var istat:int = int(status);
								var statusMessage:String = statusLine[2];
								this._httpStatus = istat;
								this._statusLine = statusMessage;
								logger.debug( _id + ": HTTP Status: " + istat + ": " + statusMessage );
								dispatchEvent(new HTTPStatusEvent (HTTPStatusEvent.HTTP_STATUS, false, false, istat));
							}
	                	}
            		}                                                    
	            } catch(e:EOFError) {
	            	break;
	            }     
	        }
		}       

		/**
		 * Searches for the end-of-header marker in the respone buffer.  The headers
		 * are terminated with the sequence \r\n\r\n.
		 */
		private function findDelimiter():int {
			// Look for \r\n\r\n in the byte array.
			for( var i:int; i<_responseBuffer.length-3; i++ ) {
//				logger.debug( "find: " + _responseBuffer[i] + "," +  _responseBuffer[i+1] +
//				 "," + _responseBuffer[i+2] + "," + _responseBuffer[i+3] );
				 
				if( _responseBuffer[i] == 13 && _responseBuffer[i+1] == 10
					&& _responseBuffer[i+2] == 13 &&
					_responseBuffer[i+3] == 10 ) {
						return i;
					}
			}
			
			return -1;
		}      
		
		/**
		 * Send the HTTP request and headers.
		 */
		private function sendHeaders():void {
			var req:String = "";
			   
			_responseHeaders = null;
			_bytesLoaded = 0;
			_bytesTotal = 0;
			if( _responseData == null ) {
				_responseData = new ByteArray();
			}
			_responseData.position = 0;
			
			_responseHeadersRead = false;
			_responseBuffer = new ByteArray();
			                               
			// Create the request string
			logger.debug( _id + ": URI: " + _uri );
			
			req+= this._requestMethod + " " + URI.fastEscapeChars( uri.path, URIpathExcludedBitmap);
			if( uri.queryRaw != null && uri.queryRaw.length > 0 ) {
				req += "?" + uri.queryRaw;
			}
			req+= " HTTP/1.0\r\n";
			req+= "Host:" + _uri.authority + ':' + _uri.port + "\r\n";
			
			// Add the headers
			if(_requestHeaders.length > 0) {
			        for each(var rh:URLRequestHeader in _requestHeaders) {
			                req+= rh.name + ":" + rh.value + "\r\n";
			        }
			}
			
			logger.debug( _id + ": sendHeaders: " + req ); 
			
			// Fire a RestRequestEvent
			if( traceEnabled ) {
				TraceDispatcher.getInstance().dispatchEvent( 
					new RestRequestEvent( this, _uri.authority, int(_uri.port),
					this._requestMethod, _uri.path + 
					(_uri.query==null?"":("?"+_uri.query)), 
					requestHeaders, req ));
			}
			// Send the request
			_socket.writeUTFBytes( req + "\r\n" )
			
			// If the request has data, send it.
			if ( _requestData ) {
				var ba:ByteArray = ByteArray( _requestData );
				_socket.writeBytes( ba, 0, ba.length );
			}
			
			// Flush the socket
			_socket.flush()
			logger.debug( _id + ": Socket flushed" );    
			
			// Add a timeout trigger to see if data is available.  Due to
			// bug http://bugs.adobe.com/jira/browse/SDK-14813 we sometimes
			// don't get data events.
			_dataPump = new Timer( 500, 0 );
			_dataPump.addEventListener( TimerEvent.TIMER, onDataPump );
			_dataPump.start();     
		}
		
		/**
		 * Called when the data pump timer expires.  Do a manual check
		 * to see if bytes are available on the socket.
		 */
		private function onDataPump( event:TimerEvent ):void {
			// See if bytes are available to read.
			logger.debug( _id + ": " + _socket.bytesAvailable + " available" );
			if( _socket.bytesAvailable > 0 ) {
				logger.debug( _id + ": manually pumping data" );
				onSocketDataEvent( new ProgressEvent( "" ) );
			}
		}
		
		/**
		 * Starts the timeout timer
		 */
		private function startTimeout():void {
			if(_requestTimeOut < 1){
			        return;
			}
			_timeoutTimer = new Timer( _requestTimeOut * 1000 );
			_timeoutTimer.addEventListener( TimerEvent.TIMER_COMPLETE,timeoutComplete );
			_timeoutTimer.start();
		}
		   
		/**
		 * Stops the timeout timer..
		 */
		private function stopTimeout():void {
			if( _timeoutTimer != null ) {
	        	_timeoutTimer.stop();
	  		}
		}
		
		/**
		 * Called when timeout expires on the socket
		 */
		private function timeoutComplete():void {
	        dispatchEvent(new IOErrorEvent(IOErrorEvent.NETWORK_ERROR,false,false,"Request timed out."));
	        _socket.close();
	        if( _dataPump != null ) {
	  			_dataPump.stop();
	  		}

		}
		
		/**
		 * Initiates the HTTP request
		 * @request the URL request to load.
		 */
		public function load():void {

			_requestHeaders.push( new URLRequestHeader('Accept',
				'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5'));
			
			// Tell the server to close the socket when done.
			_requestHeaders.push( new URLRequestHeader( 'Connection', 'Close' ) );
			   
			startTimeout();
			
			if( _requestData ) {
				_requestHeaders.push( new URLRequestHeader('Content-Length', ""+ByteArray(_requestData).length) );
				
			} else {
				_requestHeaders.push( new URLRequestHeader('Content-Length','0') );
			}
			
			if( _uri.scheme == "https" ) {
				trace( "Creating TLS socket for HTTPS" );
                var clientConfig:TLSConfig = new TLSConfig(TLSEngine.CLIENT, 
                                                           null, 
                                                           null, 
                                                           null, 
                                                           null, 
                                                           null, 
                                                           TLSSecurityParameters.PROTOCOL_VERSION);
                clientConfig.trustAllCertificates = true;
                clientConfig.trustSelfSignedCertificates = true;
                clientConfig.ignoreCommonNameMismatch = true;
				_socket = new TLSSocket( null, 0, clientConfig );
				if( _uri.port == null ) {
					_uri.port = ""+443;
				}
				
				logger.debug( _id + ": created SSL socket: " + _socket );
			} else {
				_socket = new Socket();
			}
	
	        // subscribe to events
	        _socket.addEventListener( "connect" , onConnectEvent );                        
	        _socket.addEventListener( "socketData" , onSocketDataEvent );
	        _socket.addEventListener( "close" , onCloseEvent );
	        _socket.addEventListener( "ioError" , onIOErrorEvent );
	        _socket.addEventListener( "securityError" , onSecurityErrorEvent );
	        
			_socket.connect( _uri.authority, Number(_uri.port) );        
		}
		
		/**
		 * Manually closes the socket connection
		 */
		public function close():void {
	        if(_socket.connected) {
                _socket.close();
                dispatchEvent(new Event(Event.CLOSE));
	        }
	        if( _dataPump != null ) {
	  			_dataPump.stop();
	  		}

		}
    }
}